<!DOCTYPE html><head>
<title>LittleJS Particle System Designer</title>
<meta charset=utf-8>
<meta name=apple-mobile-web-app-capable content=yes>
<meta name=mobile-web-app-capable content=yes>
<link rel=icon type=image/png href=../favicon.png>
<style>
input { width:99px; }
button { margin:1px; }
body { color:#fff; }
#div_emitterSettings
{
    position:absolute;
    z-index: 1;
    height: 100vh;
    overflow:auto;
    padding:5px;
    font-family:sans-serif;
    column-count: 2;
    column-fill: auto;
}
</style>
</head><body>
<div id=div_emitterSettings></div>
<script type=module>

// import LittleJS module
import * as LJS from '../../dist/littlejs.esm.js';
const {vec2} = LJS;

'use strict';

// fix texture bleeding by shrinking tiles slightly
LJS.setTileDefaultBleed(.5);

// allow html input controls
LJS.setInputPreventDefault(false);

let particleEditor = 0, particleSystem, particleSystemDiv, particleSystemCode;
let inputExpand;
let restartTimer = new LJS.Timer();
const storagePrefix = 'particles_';

const particleSettings = [];
function addSetting(name, type, min, max, step, description)
{
    particleSettings.push({name, type:type||'number', min, max, step, description});
}

addSetting('emitSize',         '', 0, 1e9, .1, 'World space size of the emitter');
addSetting('emitTime',         '', 0, 1e9, .1, 'How long to stay alive (0 is forever)');
addSetting('emitRate',         '', 0, 1e9, 1,  'How many particles per second to spawn, does not emit if 0');
addSetting('emitConeAngle',    '', 0, 1e9, .01,'Local angle to apply velocity to particles from emitter');
addSetting('tileIndex',        '', -1, 1e9, 1, 'Tile to use to render object (-1 is untextured)');
addSetting('tileSize',         '', 0, 1e9, 1,  'Size of texture tile in source pixels');
addSetting('colorStartA',      'color',0,0,0, 'Color at start of life 1');
addSetting('colorStartA_alpha', 'alpha', 0,    1,   .01, 'Alpha at start of life 1');
addSetting('colorStartB',      'color',0,0,0, 'Color at start of life 2');
addSetting('colorStartB_alpha', 'alpha', 0,    1,   .01, 'Alpha at start of life 2');
addSetting('colorEndA',        'color',0,0,0, 'Color at end of life 1');
addSetting('colorEndA_alpha',   'alpha', 0,    1,   .01, 'Alpha at end of life 1');
addSetting('colorEndB',        'color',0,0,0, 'Color at end of life 2');
addSetting('colorEndB_alpha',   'alpha', 0,    1,   .01, 'Alpha at end of life 2');
addSetting('particleTime',     '', 0,    1e9, .1, 'How long particles live');
addSetting('sizeStart',        '', 0,    1e9, .01,'How big are particles at start');
addSetting('sizeEnd',          '', 0,    1e9, .01,'How big are particles at end');
addSetting('speed',            '', 0,    1e9, .01,'How fast are particles when spawned');
addSetting('angleSpeed',       '', 0,    1e9, .01,'How fast are particles rotating');
addSetting('damping',          '', 0,    1,   .01,'How much to dampen particle speed');
addSetting('angleDamping',     '', 0,    1,   .01,'How much to dampen particle angular speed');
addSetting('gravityScale',     '', -1e9, 1e9, .1, 'How much does gravity effect particles');
addSetting('particleConeAngle','', 0,    1e9, .1, 'Cone for start particle angle');
addSetting('fadeRate',         '', 0,    1,   .01,'How quickly particles fade in percent of life');
addSetting('randomness',       '', 0,    1,   .01,'Apply extra randomness percent');
addSetting('additive',         'checkbox',0,0,0,  'Should particles use additive blend?');
addSetting('randomColorLinear','checkbox',0,0,0,  'Should color be randomized linearly?');

function makeEmitter()
{
    if (particleSystem)
        particleSystem.destroy();
    particleSystem = new LJS.ParticleEmitter(LJS.cameraPos);
    particleSystem.tileInfo = LJS.tile();
    particleSystem.emitConeAngle =
    particleSystem.particleConeAngle = 3.14;
}

///////////////////////////////////////////////////////////////////////////////
function gameInit()
{
    makeEmitter();
    LJS.setGravity(vec2(0,-.01));
    LJS.setCameraScale(64);

    const div = div_emitterSettings;
    const title = document.createElement('span')
    title.innerText = 'LittleJS Particle Tool';
    title.style = 'font-size:20px;font-weight:bold;';
    div.appendChild(title);

    for (const setting of particleSettings)
    {
        // get default value
        // todo: clean this up
        let defaultValue = '';
        {
            const name = setting.name;
            const type = setting.type;
            
            if (type == 'color')
            {
                const color = particleSystem[name];
                defaultValue = color.toString(false);
            }
            else if (type == 'alpha')
            {
                if (name == 'colorStartA_alpha')
                    defaultValue = particleSystem.colorStartA.a;
                else if (name == 'colorStartB_alpha')
                    defaultValue = particleSystem.colorStartB.a;
                else if (name == 'colorEndA_alpha')
                    defaultValue = particleSystem.colorEndA.a;
                else if (name == 'colorEndB_alpha')
                    defaultValue = particleSystem.colorEndB.a;
            }
            else if (name == 'tileSize')
                defaultValue = particleSystem.tileInfo.size.x;
            else if (type == 'checkbox')
                defaultValue = particleSystem[name];
            else
                defaultValue = particleSystem[name] || '0';
        }

        div.appendChild(document.createElement('br'));

        const input = setting.input = document.createElement('input');
        const name = setting.name;
        input.type = setting.type == 'alpha' ? 'number' : setting.type;
        input.title = setting.description;
        input.oninput = (e)=> updateParticles(setting.type == 'checkbox');
        input.onblur = (e)=> updateParticles(1);
        if (setting.min !== undefined)
            input.min = setting.min;
        if (setting.max !== undefined)
            input.max = setting.max;
        if (setting.step !== undefined)
            input.step = setting.step;
        if (name == 'emitConeAngle' || name  == 'particleConeAngle')
            particleSystem[name] = 3.14; // simplify pi
        
        const inputReset = document.createElement('button');
        inputReset.innerText = '♻️';
        inputReset.onclick = (e)=> 
        {
            setting.type == 'checkbox'? 
                setting.input.checked = defaultValue : 
                setting.input.value = defaultValue;
            updateParticles(1);
        }
        div.appendChild(inputReset);

        div.appendChild(input);
        div.appendChild(document.createTextNode(' ' + name));
    }

    div.appendChild(document.createElement('br'));
    div.appendChild(document.createElement('br'));

    {
        const button = document.createElement('button')
        div.appendChild(button);
        button.innerHTML = 'Copy To Clipboard';
        button.onclick = (e)=> navigator.clipboard.writeText(particleSystemCode.value);
    }
    {
        const button = document.createElement('button');
        div.appendChild(button);
        button.innerHTML = 'Reset';
        button.onclick = (e)=>
        {
            if (!confirm('Reset to default?'))
                return;
            makeEmitter();
            updateInputs();
            LJS.setCameraScale(50);
        }
    }
    {
        inputExpand = document.createElement('input');
        div.appendChild(inputExpand);
        inputExpand.type = 'checkbox';
        inputExpand.style.width = 'initial';
        inputExpand.oninput = (e)=> updateParticles(1);
        div.appendChild(document.createTextNode(' Expand'));
    }
    
    //div.appendChild(document.createTextNode('JavaScript Code... '));
    div.appendChild(document.createElement('br'));
    div.appendChild(particleSystemCode = document.createElement('textarea'));
    particleSystemCode.style.width = '300px';
    particleSystemCode.style.height = '100px';
    particleSystemCode.disabled = true;

    updateInputs(0);

    // load from storage
    for (const setting of particleSettings)
    {
        const type = setting.type;
        const storageName = storagePrefix + setting.name;
        const savedValue = localStorage[storageName];
        if (savedValue == undefined)
            continue;

        if (type == 'checkbox')
            setting.input.checked = savedValue == 'true';
        else
            setting.input.value = savedValue;
    }

    updateParticles();
    particleSystemCode.value = getCode();
}

function updateParticles(shouldUpdateInput)
{
    for (const setting of particleSettings)
    {
        const input = setting.input;
        const name = setting.name;
        const type = setting.type;
        const inputFloat = parseFloat(input.value) || 0;
        if (type == 'color')
        {
            const color = new LJS.Color().setHex(input.value);
            particleSystem[name].r = color.r;
            particleSystem[name].g = color.g;
            particleSystem[name].b = color.b;
        }
        else if (type == 'alpha')
        {
            if (name == 'colorStartA_alpha')
                particleSystem.colorStartA.a = LJS.clamp(inputFloat);
            else if (name == 'colorStartB_alpha')
                particleSystem.colorStartB.a = LJS.clamp(inputFloat);
            else if (name == 'colorEndA_alpha')
                particleSystem.colorEndA.a   = LJS.clamp(inputFloat);
            else if (name == 'colorEndB_alpha')
                particleSystem.colorEndB.a   = LJS.clamp(inputFloat);
        }
        else if (name == 'tileIndex')
        {
            const tileIndex = parseInt(input.value);
            particleSystem.tileIndex = tileIndex
            particleSystem.tileInfo = LJS.tile(tileIndex, particleSystem.tileInfo.size);
        }
        else if (name == 'tileSize')
            particleSystem.tileInfo.size = vec2(parseInt(input.value));
        else if (type == 'checkbox')
            particleSystem[name] = input.checked ? 1 : 0;
        else if (name == 'emitRate')
            particleSystem[name] = LJS.min(inputFloat,1e4);
        else
            particleSystem[name] = LJS.clamp(inputFloat, setting.min, setting.max);
    }
    shouldUpdateInput && updateInputs()
}

function updateInputs(save=1)
{
    for (const setting of particleSettings)
        updateSetting(setting, save);
    particleSystemCode.value = getCode();
}

function updateSetting(setting, save=1)
{
    const input = setting.input;
    const name = setting.name;
    const type = setting.type;
    
    if (type == 'color')
    {
        const color = particleSystem[name];
        input.value = color.toString(false);
    }
    else if (type == 'alpha')
    {
        if (name == 'colorStartA_alpha')
            input.value = particleSystem.colorStartA.a;
        else if (name == 'colorStartB_alpha')
            input.value = particleSystem.colorStartB.a;
        else if (name == 'colorEndA_alpha')
            input.value = particleSystem.colorEndA.a;
        else if (name == 'colorEndB_alpha')
            input.value = particleSystem.colorEndB.a;
    }
    else if (name == 'tileSize')
        input.value = particleSystem.tileInfo.size.x;
    else if (type == 'checkbox')
        input.checked = particleSystem[name];
    else
        input.value = particleSystem[name] || '0';

    if (save)
    {
        const storageName = storagePrefix + setting.name;
        localStorage[storageName] = type == 'checkbox' ? 
            input.checked : input.value;
    }
}

function getCode()
{
    const expand = inputExpand.checked;
    let code = '';
    
    // limit float precision to prevent long strings
    const trimFloat = (n, decimals=3)=>
        Number(Number(n).toFixed(decimals));

    code = 'new ParticleEmitter(';
    if (expand)
        code += '\n  vec2(), 0,\t//position, angle\n';
    else
        code += 'vec2(), 0, ';

    let count = 0;
    for (const setting of particleSettings)
    {
        const name = setting.name;
        const type = setting.type;
        let value;
        if (name == 'tileSize' || type == 'alpha')
            continue;

        if (name == 'tileIndex')
            value = `tile(${particleSystem.tileIndex}, ${particleSystem.tileInfo.size.x})`;
        else if (type == 'color')
        {
            const c = particleSystem[name];
            value = `new Color(${trimFloat(c.r)}, ${trimFloat(c.g)}, ${trimFloat(c.b)}, ${trimFloat(c.a)})`;
        }
        else if (type == 'checkbox')
            value = particleSystem[name] ? 1 : 0;
        else
            value = trimFloat(particleSystem[name]);
        if (expand)
            code += '  ';
        code += value;

        const isEnd = name == 'randomColorLinear';
        if (expand)
        {
            code += ',\t// ' + name;
            if (!isEnd)
                code += '\n';
        }
        else if (!isEnd)
            code += ', ';
    }

    if (expand)
        code += '\n); // particle emitter';
    else
        code += ');';

    return code;
}

///////////////////////////////////////////////////////////////////////////////
function gameUpdate()
{
    if (!restartTimer.isSet() && particleSystem.destroyed)
        restartTimer.set(1);
    else if (restartTimer.elapsed())
    {
        restartTimer.unset()
        makeEmitter();
        updateParticles();
    }

    if (document.activeElement == document.body)
    {
        if (LJS.mouseIsDown(0))
        {
            particleSystem.pos = LJS.mousePos;
            particleSystem.velocity = LJS.mouseDelta.scale(.3*LJS.timeDelta);
        }
        else
        {
            particleSystem.pos = vec2();
            particleSystem.velocity = vec2();
        }
        if (LJS.mouseWheel)
        {
            // zoom camera
            let cameraScale = LJS.cameraScale;
            cameraScale -= LJS.sign(LJS.mouseWheel)*cameraScale/5;
            cameraScale = LJS.clamp(cameraScale, 10, 300);
            LJS.setCameraScale(cameraScale);
        }
    }
}

///////////////////////////////////////////////////////////////////////////////
function gameUpdatePost()
{

}

///////////////////////////////////////////////////////////////////////////////
function gameRender()
{
}

///////////////////////////////////////////////////////////////////////////////
function gameRenderPost()
{
}

///////////////////////////////////////////////////////////////////////////////
// Startup LittleJS Engine
LJS.engineInit(gameInit, gameUpdate, gameUpdatePost, gameRender, gameRenderPost, ['tiles.png']);

</script>